﻿
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;

using LExperiment.NetworkLib;

namespace LExperiment.LPublisher
{
    /// <summary>
    /// a wrapper to hold LTcpChannel, make it easy to handle some events
    /// </summary>
    sealed class SubscriberChannel : IDisposable
    {
        // ========================================= //
        #region [ member variables ]

        private LTcpChannel m_socket;
        private LPublisher m_parent;
        private EndPoint m_remoteEndpoint;// cache at first to use even after the original socket has been disposed
        private bool m_disposed;

        #endregion

        // ========================================= //
        #region [ constructor and destructor ]

        public SubscriberChannel(Socket socket, LPublisher parent)
        {
            m_socket = new LTcpChannel(socket);
            m_socket.PassiveSockClosedEvtHandler += this.OnPassiveClosedCallback;
            m_remoteEndpoint = m_socket.RemoteEndpoint;

            m_parent = parent;
        }

        ~SubscriberChannel()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (isDisposing)
                {
                    m_socket.Dispose();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        #endregion

        // ========================================= //

        public void BeginRead()
        {
            m_socket.BeginReceive();
        }

        public void BeginSend(PacketHeader header, byte[] body)
        {
            m_socket.BeginSend(header, body);
        }

        public EndPoint RemoteEndpoint { get { return m_remoteEndpoint; } }

        // ========================================= //
        #region [ event operation ]

        private void OnPassiveClosedCallback(object source, EventArgs evtargs)
        {
            // cannot call ' m_socket.RemoteEndpoint' again, for this event callback is fired after the original socket has been disposed
            Console.WriteLine("[-] connection with <{0}> is removed", m_remoteEndpoint.ToString());
            m_parent.RemoveSubscriber(this);
            this.Dispose();
        }

        #endregion
    }

    sealed class LPublisher : IDisposable
    {
        // ========================================= //
        #region [ member variables ]

        private Socket m_lsnSocket;
        private IList<SubscriberChannel> m_subscribers;
        private volatile bool m_disposed;

        #endregion

        // ========================================= //
        #region [ constructor and destructor ]

        public LPublisher()
        {
            m_subscribers = new List<SubscriberChannel>();
        }

        ~LPublisher()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (isDisposing)
                {
                    if (m_lsnSocket != null)
                        m_lsnSocket.Close();

                    lock (m_subscribers)
                    {
                        foreach (SubscriberChannel subSocket in m_subscribers)
                            subSocket.Dispose();
                    }
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        #endregion

        // ========================================= //
        public void Run(int port)
        {
            m_lsnSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint lsnEndpoint = new IPEndPoint(IPAddress.Any, port);
            m_lsnSocket.Bind(lsnEndpoint);
            m_lsnSocket.Listen(10);

            BeginAccept();
        }

        public void BeginAccept()
        {
            m_lsnSocket.BeginAccept(this.OnAcceptedCallback, null);
        }

        private void OnAcceptedCallback(IAsyncResult asyncResult)
        {
            Socket newsocket = m_lsnSocket.EndAccept(asyncResult);
            Console.WriteLine("[+] accept new subscription from <{0}>", newsocket.RemoteEndPoint.ToString());

            SubscriberChannel newChannel = new SubscriberChannel(newsocket, this);
            AddSubscriber(newChannel);

            // !!!!!!!!!!!!!!!!!!!!!!!!! IMPORTANT
            // Though the publish doesn't need to read from subscriber in this simple case
            // but it still need to call "BeginReceive" to launch an asynchronous read, aiming to check "Connection Reset" event
            // allthough "Send" or "BeginSend" will throw "ConnectionReset" exception sometimes, but it cannot be guaranteed that
            // the exception will be thrown as soon as the remote peer is closed
            // so we must launch an asynchronous read operation whether or not we need the content from the remote peer
            // to receive the "Remote Peer Closed" notification as soon as possible
            // !!!!!!!!!!!!!!!!!!!!!!!!!
            newChannel.BeginRead();

            BeginAccept();
        }

        // ========================================= //
        #region [ subscribers managment ]

        public void AddSubscriber(SubscriberChannel subSocket)
        {
            if (m_disposed)
            {
                subSocket.Dispose();
                Console.WriteLine("[-] publisher has been disposed");
            }
            else
            {
                lock (m_subscribers)
                {
                    m_subscribers.Add(subSocket);
                }
            }
        }

        public void RemoveSubscriber(SubscriberChannel subSocket)
        {
            lock (m_subscribers)
            {
                m_subscribers.Remove(subSocket);
            }
        }

        public void Publish(byte[] body)
        {
            PacketHeader header = new PacketHeader();
            header.BodyLength = body.Length;

            lock (m_subscribers)
            {

                int index = 0;
                foreach (SubscriberChannel subChannel in m_subscribers)
                {
                    subChannel.BeginSend(header, body);

                    ++index;
                    Console.WriteLine("[p-{0}] send to {1}", index.ToString(), subChannel.RemoteEndpoint.ToString());
                }
            }
        }

        #endregion
    }
}